// 后面在 class Store 中还要使用的，所有要存到变量中
let Vue

// 为每一个 Vue 实例混入一个 beforeCreate 钩子，在钩子里面给每个实例挂载 $store 属性
const install = _Vue => {
  Vue = _Vue
  // 如何往组件实例身上挂一个 $store 呢？ 可以通过混入
  // 这个 Vue.mixin 就是混入配置选项，
  // 从此以后，在创建 Vue 实例时，就会自动合并这个混入的配置选项
  Vue.mixin({
    beforeCreate() {
      // beforeCreate 这个钩子的执行顺序是父组件先，子组件后
      // 生命周期函数中的 this 就是当前的 Vue 实例
      // this.$options 是当前实例的配置选项
      console.log(this.$options.name) // 先 Root 再 App
      const options = this.$options // 所以首先拿到的 Root 的选项
      // 第一次先是根实例执行 beforeCreate
      if (options.store) {
        // 所以必定父组件身上就会先绑定上 $store
        this.$store = options.store
      } else {
        // 后面的子组件就可以通过 options.parent 拿到父组件实例
        // 然后通过 $store 拿到父组件实例身上的 store
        // this.$store = options.parent && options.parent.$store
        // ?. 可选链操作符，如果前面为null或者undefined，则不会取后面的值，防止报错
        this.$store = options.parent?.$store
      }
    },
  })
}

class Store {
  // 私有属性
  // #state
  // state // 应该不能被随意更改，所以应该是只读的
  // 在页面上获取 state 是 this.$store.state.xxx
  // this.$store.state ==> 调用到 get state() 拿到其返回值
  // this.$store.state ==> this.#vm._data.$$state
  // this.$store.state.xxx ==> this.#vm._data.$$state.xxx
  get state() {
    // 因为我们 绑定在 Vue 实例 的 data 上的 名字叫 $$state
    return this.#vm._data.$$state
  }

  #mutations

  #actions

  #vm

  #getters

  // this.$store.getters.xxx 访问 vuex 的 getter 的
  // 和 state 同理，要借助 Vue 实例的 computed
  // 所以，不能直接拿原始定义的 getters
  get getters() {
    return this.#getters
  }

  constructor(options) {
    // 这个形参 options 就是 new Vuex.Store(obj) 传过来obj对象
    // this.#getters = {} // options.getters
    // this ==> 当前 new 出来的 Store 实例
    // 为什么要定义私有属性呢？因为在 constructor 外面还要用的。
    this.#getters = Object.create(null) // options.getters

    // const computed = {} // 可以点出原型链上的东东，不是一个干净的空对象
    // 因为 computed 仅仅在 constructor 这个函数中使用，在外面不需要用
    const computed = Object.create(null)

    // 这个 getterName ==> 'doubleCount'
    // 这个 getterFn ==> doubleCount 的函数
    for (const [getterName, getterFn] of Object.entries(options.getters)) {
      Object.defineProperty(this.#getters, getterName, {
        // 当你访问 this.$store.getters 的时候，会进 get getters() 函数
        // get getters() 函数返回的是 this.#getters
        // 当你 this.$store.getters.xxx ==> this.#getters.xxx ==> 就会进这个 get 函数
        // 这个 get 函数里，就会拿 #vm 这个 Vue 实例的 计算属性
        get: () => {
          // this 是 Store 实例
          // this.#vm 是 new 出来 Vue 实例
          // this.#vm[getterName] 就是在访问 Vue 实例身上的 计算属性
          // 那么就会调用到 computed 身上的 getterName 这个函数
          //  这个函数的返回值就是计算属性的值
          //    这个函数内部会调用你在 new Vuex.Store({ getters }) 这个配置中的 getters 中的对应的 getter 函数
          return this.#vm[getterName]
        },
      })
      // 每次循环，computed 对象上也会添加一个计算属性配置
      computed[getterName] = () => {
        const returnValue = getterFn(this.state, this.getters)
        return returnValue
      }
    }
    // 循环完后，computed 长这样：
    /*
     {
       doubleCount: () => {
        return getterFn(this.state, this.getters)
       }
     }
     */

    // 这个不能直接拿，因为不是响应式的，数据变了，不会驱动视图的更新，思路：借助 Vue 实例的 data
    // this.#state = options.state
    this.#vm = new Vue({
      // 之前讲组件的 data 必须是个函数，那为什么这里不是函数？
      // 因为这里对于同一个 store 实例而言 只会存在一个 Vue 实例，而组件实例是有可能存在多个的
      data: {
        // 这样 state 就是就是响应式的了
        // 以 $ 或 _ 开头的 data 名，不会挂载到组件实例身上
        $$state: options.state,
      },
      // 挂上计算属性
      computed,
    })

    this.#mutations = options.mutations
    this.#actions = options.actions
  }

  commit(mutationName, payload) {
    // this 就是 store 实例, 因为你在调用的时候是：this.$store.commit
    // this.state ==> 上面那个 get state() 取值函数的返回值
    this.#mutations[mutationName](this.state, payload)
  }

  dispatch(actionName, payload) {
    this.#actions[actionName](this, payload)
  }
}

export default {
  install,
  Store,
}
